package com.zhangfb95.crosschain.plugin.container.java.fabric2.service.impl;

import com.google.protobuf.ByteString;
import com.zhangfb95.crosschain.infra.exception.BusinessException;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.infra.config.properties.FabricProperties;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.infra.data.api.crosschainserver.PluginApi;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.infra.extend.fabricsupport.FabricUser;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.infra.extend.fabricsupport.StdCryptoSuite;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.infra.extend.fabricsupport.WrappedEnrollment;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.infra.model.constant.PropertyKey;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.infra.util.CertUtil;
import com.zhangfb95.crosschain.plugin.container.java.fabric2.service.FabricService;
import com.zhangfb95.crosschain.protocol.routerplugincontroller.request.GetAccountPluginReq;
import com.zhangfb95.crosschain.protocol.routerplugincontroller.request.SignPluginReq;
import com.zhangfb95.crosschain.protocol.routerplugincontroller.response.GetAccountPluginRes;
import com.zhangfb95.crosschain.protocol.routerplugincontroller.response.SignPluginRes;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.hyperledger.fabric.protos.peer.ProposalPackage;
import org.hyperledger.fabric.protos.peer.lifecycle.Lifecycle;
import org.hyperledger.fabric.sdk.BlockEvent;
import org.hyperledger.fabric.sdk.BlockInfo;
import org.hyperledger.fabric.sdk.Channel;
import org.hyperledger.fabric.sdk.Enrollment;
import org.hyperledger.fabric.sdk.HFClient;
import org.hyperledger.fabric.sdk.LifecycleQueryInstalledChaincodesProposalResponse;
import org.hyperledger.fabric.sdk.LifecycleQueryInstalledChaincodesProposalResponse.LifecycleQueryInstalledChaincodesResult;
import org.hyperledger.fabric.sdk.LifecycleQueryInstalledChaincodesRequest;
import org.hyperledger.fabric.sdk.Orderer;
import org.hyperledger.fabric.sdk.Peer;
import org.hyperledger.fabric.sdk.ProposalResponse;
import org.hyperledger.fabric.sdk.SDKUtils;
import org.hyperledger.fabric.sdk.TransactionInfo;
import org.hyperledger.fabric.sdk.TransactionProposalRequest;
import org.hyperledger.fabric.sdk.User;
import org.hyperledger.fabric.sdk.exception.InvalidArgumentException;
import org.hyperledger.fabric.sdk.exception.ProposalException;
import org.hyperledger.fabric.sdk.transaction.ProposalBuilder;
import org.hyperledger.fabric.sdk.transaction.TransactionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Security;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * fabric客户端
 *
 * @author zhangfubing
 * @since 2023/7/6
 */
@Slf4j
@Service
public class FabricServiceImpl implements FabricService {

    @Autowired
    private FabricProperties fabricProperties;
    @Autowired
    private StdCryptoSuite stdCryptoSuite;
    @Autowired
    private PluginApi pluginApi;

    private Map<String, Peer> peerMap;
    private Map<String, Orderer> ordererMap;
    private HFClient hfClient;
    private final Map<String, Channel> channelMap = new ConcurrentHashMap<>();

    static {
        Security.setProperty("crypto.policy", "unlimited");
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 初始化fabric客户端
     */
    @PostConstruct
    public void init() {
        try {
            // 初始化hfClient
            this.hfClient = HFClient.createNewInstance();
            this.hfClient.setCryptoSuite(stdCryptoSuite);
            this.hfClient.setUserContext(buildUser());
            // 初始化节点及通道
            this.peerMap = buildPeerMap();
            this.ordererMap = buildOrdererMap();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 构建通道对象
     *
     * @param channelName 通道名称
     * @return 通道对象
     */
    @Override
    public Channel buildChannel(String channelName) {
        return buildChannel(channelName, this.peerMap, this.ordererMap);
    }

    /**
     * 获取区块高度
     *
     * @param channelName 通道名称
     * @return 区块高度
     */
    @Override
    public long getHeight(String channelName) {
        Channel channel = buildChannel(channelName, this.peerMap, this.ordererMap);
        try {
            return channel.queryBlockchainInfo().getHeight();
        } catch (Exception e) {
            throw new BusinessException(e);
        }
    }

    /**
     * 根据区块编号查询区块
     *
     * @param channelName 通道名称
     * @param blockNumber 区块编号
     * @return 区块信息
     */
    @Override
    public BlockInfo queryBlockByNumber(String channelName, long blockNumber) {
        Channel channel = buildChannel(channelName, this.peerMap, this.ordererMap);
        try {
            return channel.queryBlockByNumber(blockNumber);
        } catch (Exception e) {
            throw new BusinessException(e);
        }
    }

    /**
     * 根据区块哈希查询区块
     *
     * @param channelName 通道名称
     * @param blockHash   区块哈希
     * @return 区块信息
     */
    @Override
    public BlockInfo queryBlockByHash(String channelName, String blockHash) {
        // 解码hash值
        byte[] decodeBlockHash;
        try {
            decodeBlockHash = Hex.decodeHex(blockHash);
        } catch (DecoderException e) {
            throw new BusinessException(e);
        }

        Channel channel = buildChannel(channelName, this.peerMap, this.ordererMap);
        try {
            return channel.queryBlockByHash(decodeBlockHash);
        } catch (Exception e) {
            throw new BusinessException(e);
        }
    }

    /**
     * 根据交易ID查询交易信息
     *
     * @param channelName 通道名称
     * @param txID        交易ID
     * @return 交易信息
     */
    @Override
    public TransactionInfo queryTransactionByID(String channelName, String txID) {
        Channel channel = buildChannel(channelName, this.peerMap, this.ordererMap);
        try {
            return channel.queryTransactionByID(txID);
        } catch (Exception e) {
            throw new BusinessException(e);
        }
    }

    /**
     * 查询安装的通道链码列表
     *
     * @param channelName 通道名称
     * @return 通道链码列表
     */
    @Override
    public Map<String, Map<String, String>> queryInstalledChainCodes(String channelName) {
        return queryInstalledChainCodes(channelName, this.fabricProperties.getMspId());
    }

    /**
     * 查询安装的通道链码列表
     *
     * @param channelName 通道名称
     * @param mspId       mspId
     * @return 通道链码列表
     */
    @Override
    public Map<String, Map<String, String>> queryInstalledChainCodes(String channelName, String mspId) {
        // 如果mspId为空，那么返回空map
        if (mspId == null || mspId.trim().isEmpty()) {
            return Collections.emptyMap();
        }

        Map<String, Map<String, String>> channel2chainCode2VersionMap = new HashMap<>();

        // 过滤peer列表，只查询匹配msp的peer
        List<Peer> peers = this.peerMap.values().stream()
                .filter(peer -> mspId.equals(peer.getProperties().getProperty(Peer.PEER_ORGANIZATION_MSPID_PROPERTY)))
                .collect(Collectors.toList());

        // 查询安装的链码列表
        LifecycleQueryInstalledChaincodesRequest req = this.hfClient.newLifecycleQueryInstalledChaincodesRequest();
        Collection<LifecycleQueryInstalledChaincodesProposalResponse> responses;
        try {
            responses = this.hfClient.sendLifecycleQueryInstalledChaincodes(req, peers);
        } catch (Exception e) {
            throw new BusinessException(e);
        }

        // 遍历peer查询结果
        for (LifecycleQueryInstalledChaincodesProposalResponse response : responses) {
            // 如果无效，那么跳过
            if (response.isInvalid()) {
                continue;
            }

            // 遍历链码查询结果
            Collection<LifecycleQueryInstalledChaincodesResult> results;
            try {
                results = response.getLifecycleQueryInstalledChaincodesResult();
            } catch (ProposalException e) {
                throw new BusinessException(e);
            }

            for (LifecycleQueryInstalledChaincodesResult result : results) {
                // 反射获取可访问的字段
                Field installedChaincodeField;
                try {
                    installedChaincodeField = result.getClass().getDeclaredField("installedChaincode");
                } catch (NoSuchFieldException e) {
                    throw new RuntimeException(e);
                }
                installedChaincodeField.setAccessible(true);

                // 反射获取链码列表
                Lifecycle.QueryInstalledChaincodesResult.InstalledChaincode installedChaincode;
                try {
                    installedChaincode = (Lifecycle.QueryInstalledChaincodesResult.InstalledChaincode)
                            installedChaincodeField.get(result);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }

                // 将链码放入返回结果
                for (Map.Entry<String, Lifecycle.QueryInstalledChaincodesResult.References> entry :
                        installedChaincode.getReferencesMap().entrySet()) {
                    String curChannelName = entry.getKey();
                    Map<String, String> chaincode2VersionMap =
                            channel2chainCode2VersionMap.computeIfAbsent(curChannelName, k -> new HashMap<>());
                    if (!StringUtils.hasText(channelName) || Objects.equals(curChannelName, channelName)) {
                        Lifecycle.QueryInstalledChaincodesResult.References references = entry.getValue();
                        for (Lifecycle.QueryInstalledChaincodesResult.Chaincode chaincode :
                                references.getChaincodesList()) {
                            chaincode2VersionMap.put(chaincode.getName(), chaincode.getVersion());
                        }
                    }
                }
            }
        }

        return channel2chainCode2VersionMap;
    }

    /**
     * 发送交易
     *
     * @param serverId      服务id
     * @param userId        用户id
     * @param chainPath     链路径
     * @param channelName   通道名称
     * @param chaincodeName 链码名称
     * @param fcn           方法名称
     * @param args          参数列表
     * @return 区块交易事件信息
     */
    @Override
    public BlockEvent.TransactionEvent sendTransaction(String serverId,
                                                       Long userId,
                                                       String chainPath,
                                                       String channelName,
                                                       String chaincodeName,
                                                       String fcn,
                                                       String[] args) {
        Channel channel = buildChannel(channelName, this.peerMap, this.ordererMap);
        Collection<ProposalResponse> proposalResponses =
                sendProposalToPeers(serverId, userId, chainPath, channel, chaincodeName, fcn, args);
        for (ProposalResponse proposalResponse : proposalResponses) {
            if (proposalResponse.isInvalid()) {
                throw new BusinessException(proposalResponse.getMessage());
            }
        }

        try {
            CompletableFuture<BlockEvent.TransactionEvent> future = channel.sendTransaction(proposalResponses);
            BlockEvent.TransactionEvent transactionEvent = future.get(60, TimeUnit.SECONDS);
            if (!transactionEvent.isValid()) {
                throw new BusinessException("sendTransaction, invalid transaction");
            }
            return transactionEvent;
        } catch (Exception e) {
            throw new BusinessException(e);
        }
    }

    /**
     * 查询链码
     *
     * @param serverId      服务id
     * @param userId        用户id
     * @param chainPath     链路径
     * @param channelName   通道名称
     * @param chaincodeName 链码名称
     * @param fcn           方法名称
     * @param args          参数列表
     * @return 查询结果
     */
    @Override
    public String queryByChaincode(String serverId,
                                   Long userId,
                                   String chainPath,
                                   String channelName,
                                   String chaincodeName,
                                   String fcn,
                                   String[] args) {
        Channel channel = buildChannel(channelName, this.peerMap, this.ordererMap);
        Collection<ProposalResponse> proposalResponses =
                sendProposalToPeers(serverId, userId, chainPath, channel, chaincodeName, fcn, args);
        for (ProposalResponse proposalResponse : proposalResponses) {
            if (proposalResponse.isInvalid()) {
                throw new BusinessException(proposalResponse.getMessage());
            }
            return new String(proposalResponse.getProposalResponse().getResponse().getPayload().toByteArray());
        }
        log.error("not find valid proposalResponses.");
        throw new RuntimeException("not find valid proposalResponses.");
    }

    /**
     * 查询通道列表
     *
     * @return 通道列表
     * @throws ProposalException        提案异常
     * @throws InvalidArgumentException 无效参数异常
     */
    @Override
    public Set<String> queryChannels() throws ProposalException, InvalidArgumentException {
        Optional<Peer> optionalPeer = peerMap.values().stream().findFirst();
        if (optionalPeer.isEmpty()) {
            throw new RuntimeException("peer not found");
        }
        return this.hfClient.queryChannels(optionalPeer.get());
    }

    /**
     * 计算区块哈希
     *
     * @param blockInfo 区块信息
     * @return 区块哈希
     */
    @Override
    public byte[] calculateBlockHash(BlockInfo blockInfo) {
        try {
            return SDKUtils.calculateBlockHash(this.hfClient, blockInfo.getBlockNumber(), blockInfo.getPreviousHash(), blockInfo.getDataHash());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 构建用户
     *
     * @return 用户对象
     */
    private User buildUser() {
        Enrollment enrollment = buildEnrollment();
        return new FabricUser(this.fabricProperties.getUser(), this.fabricProperties.getMspId(), enrollment);
    }

    /**
     * 构建注册信息，包括：私钥、证书
     *
     * @return 注册信息
     */
    private Enrollment buildEnrollment() {
        PrivateKey privateKey = CertUtil.getPrivateKey(fabricProperties.getEnrollment().getKeyText());
        String certContent = fabricProperties.getEnrollment().getCertText();
        return new WrappedEnrollment(privateKey, certContent);
    }

    /**
     * 构建peerMap
     *
     * @return peerMap
     * @throws IOException              IO异常
     * @throws InvalidArgumentException 无效参数异常
     */
    private Map<String, Peer> buildPeerMap() throws IOException, InvalidArgumentException {
        Map<String, Peer> peersMap = new LinkedHashMap<>();

        int index = 0;
        for (FabricProperties.PeerInfo peerInfo : this.fabricProperties.getPeers()) {
            String name = "peer-" + index;
            peersMap.put(name, buildPeer(peerInfo, index));
            index++;
        }

        return peersMap;
    }

    /**
     * 构建peer
     *
     * @param peerInfo peer配置信息
     * @param index    索引号
     * @return peer
     * @throws IOException              IO异常
     * @throws InvalidArgumentException 无效参数异常
     */
    private Peer buildPeer(FabricProperties.PeerInfo peerInfo, int index)
            throws IOException, InvalidArgumentException {
        Properties properties = new Properties();
        properties.put("pemBytes", peerInfo.getTlsCaFile().getBytes(StandardCharsets.UTF_8));
        properties.setProperty("sslProvider", "JDK");
        properties.setProperty("negotiationType", "TLS");
        properties.setProperty("hostnameOverride", peerInfo.getHostnameOverride());
        properties.setProperty("trustServerCertificate", "true");
        properties.setProperty("allowAllHostNames", "true");
        properties.setProperty(Peer.PEER_ORGANIZATION_MSPID_PROPERTY, peerInfo.getMspId());
        return this.hfClient.newPeer("peer" + index, peerInfo.getPeerAddress(), properties);
    }

    /**
     * 构建ordererMap
     *
     * @return ordererMap
     * @throws IOException              IO异常
     * @throws InvalidArgumentException 无效参数异常
     */
    private Map<String, Orderer> buildOrdererMap() throws IOException, InvalidArgumentException {
        Map<String, Orderer> orderersMap = new LinkedHashMap<>();

        int index = 0;
        for (FabricProperties.OrdererInfo ordererInfo : fabricProperties.getOrderers()) {
            String name = "orderer-" + index;
            orderersMap.put(name, buildOrderer(ordererInfo, index));
            index++;
        }

        return orderersMap;
    }

    /**
     * 构建orderer
     *
     * @param ordererInfo orderer配置信息
     * @param index       索引号
     * @return orderer
     * @throws IOException              IO异常
     * @throws InvalidArgumentException 无效参数异常
     */
    private Orderer buildOrderer(FabricProperties.OrdererInfo ordererInfo, int index)
            throws IOException, InvalidArgumentException {
        Properties properties = new Properties();
        properties.put("pemBytes", ordererInfo.getTlsCaFile().getBytes(StandardCharsets.UTF_8));
        properties.setProperty("sslProvider", "JDK");
        properties.setProperty("negotiationType", "TLS");
        properties.setProperty("ordererWaitTimeMilliSecs", "300000");
        properties.setProperty("hostnameOverride", ordererInfo.getHostnameOverride());
        properties.setProperty("trustServerCertificate", "true");
        properties.setProperty("allowAllHostNames", "true");
        properties.setProperty(Orderer.ORDERER_ORGANIZATION_MSPID_PROPERTY, ordererInfo.getMspId());
        return this.hfClient.newOrderer("orderer" + index, ordererInfo.getOrdererAddress(), properties);
    }

    /**
     * 构建通道
     *
     * @param channelName 通道名称
     * @param peersMap    peerMap
     * @param ordererMap  ordererMap
     * @return 通道
     */
    private Channel buildChannel(String channelName, Map<String, Peer> peersMap, Map<String, Orderer> ordererMap) {
        Channel curChannel = channelMap.get(channelName);
        if (curChannel != null) {
            return curChannel;
        }

        try {
            Channel channel = this.hfClient.newChannel(channelName);
            for (Peer peer : peersMap.values()) {
                channel.addPeer(peer);
            }
            for (Orderer orderer : ordererMap.values()) {
                channel.addOrderer(orderer);
            }
            channel.initialize();
            channelMap.putIfAbsent(channelName, channel);
            return channel;
        } catch (Exception e) {
            log.error("init channel err, msg:{}", e, e);
            throw new BusinessException(e.getMessage(), e);
        }
    }

    /**
     * 发送交易提案到peer
     *
     * @param serverId      服务id
     * @param userId        用户id
     * @param chainPath     链路径
     * @param channel       通道
     * @param chaincodeName 链码名称
     * @param fcn           方法名称
     * @param args          参数列表
     * @return 提案响应集合
     */
    private Collection<ProposalResponse> sendProposalToPeers(String serverId,
                                                             Long userId,
                                                             String chainPath,
                                                             Channel channel,
                                                             String chaincodeName,
                                                             String fcn,
                                                             String[] args) {
        // 请求获取账户信息
        GetAccountPluginReq accountApiReq = new GetAccountPluginReq()
                .setServerId(serverId)
                .setUserId(userId)
                .setChainPath(chainPath)
                .setPropertyKeys(Arrays.asList(PropertyKey.NAME, PropertyKey.MSP_ID));
        GetAccountPluginRes accountApiRes = pluginApi.getAccount(accountApiReq).fallback().getData();
        String publicKey = accountApiRes.getPublicKey();
        Map<String, String> accountPropertyMap = accountApiRes.getPropertyMap();

        // 构建用户信息
        Enrollment enrollment = new WrappedEnrollment(null, publicKey);
        FabricUser user = new FabricUser(accountPropertyMap.get(PropertyKey.NAME),
                accountPropertyMap.get(PropertyKey.MSP_ID),
                enrollment);

        // 构建交易提案请求
        TransactionProposalRequest transactionProposalRequest = TransactionProposalRequest.newInstance(user);
        transactionProposalRequest.setChaincodeName(chaincodeName);
        transactionProposalRequest.setFcn(fcn);
        transactionProposalRequest.setArgs(args);

        try {
            // 构建交易上下文，重写签名方法
            TransactionContext transactionContext =
                    getTransactionContext(channel, user, transactionProposalRequest, serverId, userId, chainPath);

            // 构建交易提案
            ProposalBuilder proposalBuilder = ProposalBuilder.newBuilder();
            proposalBuilder.context(transactionContext);
            proposalBuilder.request(transactionProposalRequest);
            ProposalPackage.Proposal proposal = proposalBuilder.build();

            // 构建签名提案
            ProposalPackage.SignedProposal signedProposal = ProposalPackage.SignedProposal.newBuilder()
                    .setProposalBytes(proposal.toByteString())
                    .setSignature(ByteString.copyFrom(transactionContext.sign(proposal.toByteArray())))
                    .build();

            // 请求peer进行提案背书
            Method sendProposalToPeers = Channel.class.getDeclaredMethod(
                    "sendProposalToPeers",
                    Collection.class, ProposalPackage.SignedProposal.class, TransactionContext.class);
            sendProposalToPeers.setAccessible(true);
            Object proposalResponses = sendProposalToPeers.invoke(
                    channel, channel.getPeers(), signedProposal, transactionContext);
            return (Collection<ProposalResponse>) proposalResponses;
        } catch (Exception e) {
            log.error("sendProposalToPeers err:{}", e, e);
            throw new BusinessException(e.getMessage(), e);
        }
    }

    /**
     * 生成交易上下文，重写签名方法
     *
     * @param channel                    通道
     * @param user                       用户
     * @param transactionProposalRequest 交易提案请求
     * @param serverId                   服务id
     * @param userId                     用户id
     * @param chainPath                  链路径
     * @return 交易上下文
     */
    private TransactionContext getTransactionContext(Channel channel,
                                                     FabricUser user,
                                                     TransactionProposalRequest transactionProposalRequest,
                                                     String serverId,
                                                     Long userId,
                                                     String chainPath) {
        TransactionContext transactionContext =
                new TransactionContext(channel, user, this.hfClient.getCryptoSuite()) {
                    @Override
                    public byte[] sign(byte[] b) {
                        SignPluginReq apiReq = new SignPluginReq()
                                .setServerId(serverId)
                                .setUserId(userId)
                                .setChainPath(chainPath)
                                .setBytes(b);
                        SignPluginRes apiRes = pluginApi.sign(apiReq).fallback().getData();
                        return apiRes.getSignedBytes();
                    }
                };
        transactionContext.setProposalWaitTime(transactionProposalRequest.getProposalWaitTime());
        return transactionContext;
    }
}